﻿using System.Collections.Generic;
using JetBlack.Diagnostics;
using JetBlack.FeedBus.Messages;

namespace JetBlack.FeedBus.Distributor
{
    internal class SubscriptionCache : Dictionary<string, List<Interactor>> { }
    internal class FeedCache : Dictionary<string, SubscriptionCache> { }

    internal class SubscriptionManager
    {
        internal delegate void OnSubscriptionHandler(ForwardedSubscriptionRequest message);
        internal event OnSubscriptionHandler OnSubscription;

        internal delegate void OnClientMessageHandler(Interactor publisher, Interactor subscriber, ClientMessage message);
        internal event OnClientMessageHandler OnClientMessage;

        internal delegate void OnSubscriberMessageHandler(Interactor publisher, Interactor subscriber, SubscriberMessage message);
        internal event OnSubscriberMessageHandler OnSubscriberMessage;

        internal delegate void OnStaleTopicHandler(Interactor publisher, Interactor subscriber, string feed, string topic);
        internal event OnStaleTopicHandler OnStaleTopic;

        private readonly FeedCache feedCache = new FeedCache();

        internal void HandleClientMessage(ClientMessage message, Interactor publisher)
        {
            if (feedCache.ContainsKey(message.Feed) && feedCache[message.Feed].ContainsKey(message.Topic) && OnClientMessage != null)
            {
                foreach (Interactor subscriber in feedCache[message.Feed][message.Topic])
                    if (message.ClientId == subscriber.Id)
                        OnClientMessage(publisher, subscriber, message);
            }
        }

        internal void HandleSubscriberMessage(SubscriberMessage message, Interactor publisher)
        {
            if (feedCache.ContainsKey(message.Feed) && feedCache[message.Feed].ContainsKey(message.Topic) && OnSubscriberMessage != null)
            {
                foreach (Interactor subscriber in feedCache[message.Feed][message.Topic])
                    OnSubscriberMessage(publisher, subscriber, message);
            }
        }

        internal void HandleSubscription(SubscriptionRequest message, Interactor subscriber)
        {
            AppEnvironment.TraceSource.Verbose("Received subscription from {0} on \"{1}\"", subscriber, message);

            if (message.IsAdd)
                AddSubscription(message.Feed, message.Topic, subscriber);
            else
                RemoveSubscription(message.Feed, message.Topic, subscriber);
        }

        private void AddSubscription(string feed, string topic, Interactor subscriber)
        {
            if (!feedCache.ContainsKey(feed))
                feedCache.Add(feed, new SubscriptionCache());

            // Prepare the feed/topic/field/value area
            if (!feedCache[feed].ContainsKey(topic))
                feedCache[feed].Add(topic, new List<Interactor>());

            // Find the list of interactors that have subscribed to this topic.
            List<Interactor> subscribers = feedCache[feed][topic];
            if (!subscribers.Contains(subscriber))
                subscribers.Add(subscriber);

            // Inform those waiting for this request
            NotifyListeners(subscriber.Id, feed, topic, true);
        }

        private void RemoveSubscription(string feed, string topic, Interactor subscriber)
        {
            SubscriptionCache subscriptionCache;
            if (!feedCache.TryGetValue(feed, out subscriptionCache))
                return;

            // Prepare the feed/topic/field/value area
            List<Interactor> subscribers;
            if (!subscriptionCache.TryGetValue(topic, out subscribers))
                return;

            // Find the list of interactors that have subscribed to this topic.
            if (subscribers.Contains(subscriber))
                subscribers.Remove(subscriber);

            if (subscribers.Count == 0)
            {
                subscriptionCache.Remove(topic);

                if (feedCache.Count == 0)
                    feedCache.Remove(feed);
            }

            // Inform those waiting for this request
            NotifyListeners(subscriber.Id, feed, topic, false);
        }

        internal void Remove(Interactor subscriber)
        {
            AppEnvironment.TraceSource.Verbose("Removing subscriptions for {0}", subscriber);

            List<string> emptyFeeds = new List<string>();
            foreach (KeyValuePair<string, SubscriptionCache> feedItem in feedCache)
            {
                List<string> emptySubscriptions = new List<string>();

                foreach (KeyValuePair<string, List<Interactor>> subscriptionItem in feedItem.Value)
                {
                    if (subscriptionItem.Value.Contains(subscriber))
                    {
                        subscriptionItem.Value.Remove(subscriber);
                        if (subscriptionItem.Value.Count == 0)
                            emptySubscriptions.Add(subscriptionItem.Key);
                        NotifyListeners(subscriber.Id, feedItem.Key, subscriptionItem.Key, false);
                    }
                }

                foreach (string topic in emptySubscriptions)
                    feedItem.Value.Remove(topic);

                if (feedItem.Value.Count == 0)
                    emptyFeeds.Add(feedItem.Key);
            }

            foreach (string feed in emptyFeeds)
                feedCache.Remove(feed);
        }

        private void NotifyListeners(int clientId, string feed, string topic, bool isAdd)
        {
            if (OnSubscription != null)
                OnSubscription(new ForwardedSubscriptionRequest(clientId, feed, topic, isAdd));
        }

        internal void HandleNotificationResponse(Interactor notifiable, IEnumerable<string> feeds, bool isPublisher, bool isAdd)
        {
            if (isAdd)
                HandleActivePublisher(notifiable, feeds);
            else if (isPublisher)
                // Only notifiers which provide publishing services should generate stale events.
                HandleStalePublisher(notifiable, feeds);
        }

        private void HandleActivePublisher(Interactor notifiable, IEnumerable<string> feeds)
        {
            foreach (string feed in feeds)
                if (feedCache.ContainsKey(feed))
                    foreach (KeyValuePair<string, List<Interactor>> subscriptionItem in feedCache[feed])
                        foreach (Interactor subscriber in subscriptionItem.Value)
                            notifiable.SendMessage(new ForwardedSubscriptionRequest(subscriber.Id, feed, subscriptionItem.Key, true));
        }

        private void HandleStalePublisher(Interactor publisher, IEnumerable<string> feeds)
        {
            foreach (string feed in feeds)
                if (feedCache.ContainsKey(feed))
                    foreach (KeyValuePair<string, List<Interactor>> subscriptionItem in feedCache[feed])
                        foreach (Interactor subscriber in subscriptionItem.Value)
                            OnStaleTopic(publisher, subscriber, feed, subscriptionItem.Key);
        }
    }
}
